/* License Notice:
 *
 * This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 *   along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * @file demo_level.cpp
 * @author TooOld2Rock'nRoll
 * @see demo_level.hpp
 */
/*---- Includes ----*/
#include "demo_level.hpp"
#include "demo_level_config.h"

#include "global.hpp"
#include "toolbox/debug.h"

/*---- Global Variables ----*/
const unsigned u_pls_y_start_position[PLAYERS_COUNT] = {PLAYER1_Y_START_POSITION, PLAYER2_Y_START_POSITION};


/*---- Methods Implementation ----*/
DemoLevel::DemoLevel () : Level(LEVEL_WIDTH, LEVEL_HEIGHT), ui_ground_lvl(GROUND_LEVEL)
{
    Texture *loading_texture = nullptr;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    //each level may have its own set of shaders, we'll use the common shader for this demo.
    this->p_common_shader = this->p_res_mgr_instance->getBasicShader (this->_res_domain_id);

    //we will also be using the default camera view as it suits our needs just fine
    this->p_view_window = this->p_res_mgr_instance->getDefaultCamera (this->_res_domain_id);
    this->p_view_window->setWorldSize (this->ui_lvl_width, this->ui_lvl_height);

    this->p_ev_mgr_instance->listenForEvent (this->id_normal_ev_listener, SDL_WindowEventID::SDL_WINDOWEVENT_SIZE_CHANGED);

    this->id_high_ev_listener = this->p_ev_mgr_instance->addListener ((EventManager::EventListenerCallback)this->highPriorityEvents_cb, this);
    this->p_ev_mgr_instance->listenForEvent (this->id_high_ev_listener, (game_event_id_e)EV_CHARACTER_WALK_STEP);
    this->p_ev_mgr_instance->listenForEvent (this->id_high_ev_listener, (game_event_id_e)EV_CHARACTER_RUN_STEP);
    this->p_ev_mgr_instance->listenForEvent (this->id_high_ev_listener, (game_event_id_e)EV_CHARACTER_LANDED);

    loading_texture = this->p_res_mgr_instance->getTexture (this->_res_domain_id, "res/img/Loading.png");
    if (!loading_texture->getTileMap ())
        loading_texture->createTileMap (7, 7, {1, 0, 0, 0});
    this->_loading_animation.setTexture (loading_texture);
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); //to keep the image pixelated
    this->_loading_animation.addContiguousTxTilesToList (0, 43);
    this->_loading_animation.getSize().setScaleFactor (6.f);
    this->_loading_animation.setAnimationFPS (13.f, true);

    this->font_managore = this->p_res_mgr_instance->getFont (this->_res_domain_id, "res/fonts/managore_m5x7.ttf", 64);
    this->sp_textbox.setFont (this->font_managore);
    this->sp_textbox.getColorTint().set (glm::vec4 (1.f, 0.f, 0.f, 1.f));
    this->sp_textbox.setString ("ArcadeFighter");
}//End Constructor

DemoLevel::~DemoLevel ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    if (this->soundtrack)
        delete this->soundtrack;
    if (this->p_char_walk_step)
        delete this->p_char_walk_step;
    if (this->p_char_run_step)
        delete this->p_char_run_step;
    if (this->p_char_land)
        delete this->p_char_land;

    if (this->background)
        delete (background);
}//End Destructor


/**
 * @brief DemoLevel follows a strict state machine.
 *
 *         ⮦ ← ← ← ← ← ← ← ← ← ← ← ← ⮦ ← ← ← ← ← ← ← ← ← ← ← ← ← ← ↰ ← ← ← ← ← ← ← ← ↰
 *         ↓                         ↓                             ↑                 ↑
 * UNKNOWN ↳ LOADING [→ EXIT_THREAD] ↳ STARTING ↱ PLAYING → ENDING → OVER → FINISHED → Ø
 *                                   ↓           ← PAUSE ↲         ↓      ↑
 *                                   ↳  →  →  →  ↳  →  →   ↳ →  →  ↳   →  ⮥
 *
 * @param new_state - the state to change to.
 *
 * @return <b>true</b> if state change was successful, <b>false</b> if <i>not</i> possible.
 */
bool DemoLevel::setState (level_state_e new_state)
{
    bool error = false;

    _debug (TRACE, "::%s (st=%u)", __FUNCTION__, new_state);

    //Level state changes and setup are not as complex as in the Character class, a simple switch should suffice.
    switch (new_state)
    {
        case level_state_e::LEVEL_LOADING:
        {
            glm::vec3 sizes = glm::vec3 (0.f);
            ResourceManager::global_status &global_st = this->p_res_mgr_instance->getGlobalStatus ();

            if (this->_current_state != level_state_e::_level_state_max &&
                    this->_current_state != level_state_e::FINISHED)
            {
                error = true;
                break;
            }//end if (this->_current_state != level_state_e::_level_state_max && ...

            //instead of creating a painted sprite, we change the clear screen color
            glClearColor (RGB2Float (77), RGB2Float (195), RGB2Float (141), 1.f);

            this->p_view_window->setWindowSize (global_st.u_window_width, global_st.u_window_height);

            //setup common shader
            this->p_common_shader->setOrthographicProjection (0.f, 0.f, global_st.u_window_width, global_st.u_window_height);
            this->p_common_shader->setCameraView (glm::mat4 (1.f)); //ignore camera view

            //in case there are loading animations for the players....
            for (auto pl_char : this->ap_characters)
                pl_char->setState (character_state_t::LOADING);

            //use player one puppet as animation for screen
            sizes = this->ap_characters[0]->getActiveSprite().getSize().getScaled ();
            this->ap_characters[0]->getPosition().set ((global_st.u_window_width - sizes.x) / 2.f,
                                                       (global_st.u_window_height - sizes.y) / 2.f);

            this->_loading_animation.getPosition().set ((float)global_st.u_window_width - sizes.x,
                                                        (float)global_st.u_window_height - sizes.y);

            //print title somewhere above the player animation...
            sizes = this->sp_textbox.getSize().getScaled ();
            this->sp_textbox.getPosition().set ((global_st.u_window_width - sizes.x) / 2.f,
                                                global_st.u_window_height / 3.f);

            break;
        } //end case level_state_e::LEVEL_LOADING:

        case level_state_e::EXIT_THREAD:
            if (this->_current_state != level_state_e::LEVEL_LOADING)
                error = true;

            //do nothing, we need to exit the thread....

            break;
        //end case level_state_e::EXIT_THREAD:

        case level_state_e::STARTING:
        {
            int i = 0;
            glm::vec3 pl_size = glm::vec3 (0.f);

            if (this->_current_state != level_state_e::LEVEL_LOADING &&
                    this->_current_state != level_state_e::EXIT_THREAD &&
                    this->_current_state != level_state_e::ENDING)
            {
                error = true;
                break;
            }//end if (this->_current_state != level_state_e::LEVEL_LOADING && ...

            //reset the clear screen color...
            glClearColor (0.f, 0.f, 0.f, 1.f);

            this->p_view_window->fitToWindow (true);
            this->p_common_shader->setCameraView (this->p_view_window->getViewMatrix ());

            this->p_audio_instance->setWorldDistances (MIN_CAMERA_WINDOW_WIDTH/4.f, this->ui_lvl_height);

            for (i = PLAYERS_COUNT; i--; )
            {
                this->ap_characters[i]->setState (character_state_t::OPENING_ANIME);

                pl_size = this->ap_characters[i]->getActiveSprite().getSize().getScaled ();
                this->ap_characters[i]->getPosition().set (u_pls_y_start_position[i] - pl_size.x/2,
                                                           GROUND_LEVEL - pl_size.y);
            }//end for (i = PLAYERS_COUNT; i--; )

            //we should have two players, so one targets the other.
            this->ap_characters[0]->setTarget (this->v3pa_character_position[1]);
            this->ap_characters[1]->setTarget (this->v3pa_character_position[0]);

            this->p_audio_instance->play (this->soundtrack);
            break;
        }//end case level_state_e::STARTING:

        case level_state_e::PAUSE:
            if (this->_current_state != level_state_e::PLAYING &&
                    this->_current_state != level_state_e::STARTING)
                error = true;
            break;
        //end case level_state_e::PAUSE:

        case level_state_e::PLAYING:
            if (this->_current_state != level_state_e::PAUSE &&
                    this->_current_state != level_state_e::STARTING)
                error = true;
            break;
        //end case level_state_e::PLAYING:

        case level_state_e::ENDING:
            this->p_audio_instance->stopMusic ();

            if (this->_current_state != level_state_e::PLAYING &&
                    this->_current_state != level_state_e::PAUSE)
                error = true;
            break;
        //end case level_state_e::ENDING:

        case level_state_e::OVER:
            if (this->_current_state != level_state_e::ENDING)
                error = true;
            break;
        //end case level_state_e::OVER:

        //Any state can come to Finish!
        case level_state_e::FINISHED:
            //be sure background working thread is not undead
            this->_joinThread ();

            this->p_audio_instance->halt ();
            break;
        //end case level_state_e::FINISHED:

        default:
            error = true; //This should never be possible!
    }//end switch (new_state)


    if (!error)
        this->_current_state = new_state; //we can only get here if all the consistency checks were ok!
#ifdef ENABLE_DEBUG
    else
        _debug (ERROR, "ERROR: invalid state %u!", new_state);
#endif

    return !error;
}//End setState ()


void DemoLevel::_loadResourcesThread_cb ()
{
    OpenGl *gl_instance = &OpenGl::instance ();
    GLsync fenceId = nullptr;

    //activate the OpenGl background context
    fenceId = gl_instance->lockBackgroundContext ();

    /* Load the necessary resources */
    this->background->load ();

    this->soundtrack = new Audio::MusicFile (this->p_res_mgr_instance->getMusic (this->_res_domain_id,
                                                                             "res/audio/CriticalTheme_Loopable.flac"));
    this->soundtrack->loop (true);

    this->p_char_walk_step = new Audio::Effect (this->p_res_mgr_instance->getSoundEffect (this->_res_domain_id,
                                                             "res/audio/CharacterFootsteps/Walk/Dirt/DIRT - Walk 3.wav"));
    this->p_char_run_step = new Audio::Effect (this->p_res_mgr_instance->getSoundEffect (this->_res_domain_id,
                                                         "res/audio/CharacterFootsteps/Run/Dirt/DIRT - Run Short 1.wav"));
    this->p_char_land = new Audio::Effect (this->p_res_mgr_instance->getSoundEffect (this->_res_domain_id,
                                                     "res/audio/CharacterFootsteps/Jump/Grass/GRASS - Post Jump 6.wav"));
    this->p_char_land->setImportance (true);

    //make sure the OpenGl context is stable and ready before proceeding
    gl_instance->unlockBackgroundContext (fenceId);

    //just to let the loading animation roll for a little longer....
    sleep (3);

    //Level loaded successfully
    this->setState (level_state_e::EXIT_THREAD);
}//end _loadResourcesThread_cb ()

void DemoLevel::_joinThread ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //be sure resources finished loading and ditch thread
    if (this->p_loading_thread)
    {
         _debug (INFO, "Killing the DemoLevel resource loading thread, waiting to finish jobs...");
        if (this->p_loading_thread->joinable ())
            this->p_loading_thread->join ();
         _debug (INFO, "DemoLevel resource loading thread rejoined!");

        delete this->p_loading_thread;
        this->p_loading_thread = nullptr;
    }//end if (this->p_loading_thread)
}//end _joinThread ()

void DemoLevel::load ()
{

    _debug (TRACE, "::%s ()", __FUNCTION__);

    /* change state to loading before doing anything else */
    this->setState (level_state_e::LEVEL_LOADING);

    /*** create basic things for the correct execution of the level ***/
    //create game objects, but only load resources in the thread
    this->background = new DemoLevelBackground (this->_res_domain_id, this->p_common_shader);

    //free the UI to use a loading animation
    //deleted at base class destructor if still valid.
    this->p_loading_thread = new std::thread (&DemoLevel::_loadResourcesThread_cb, this);
}//End load ()


/**
 * @brief Update the level camera position based on the players position.
 */
void DemoLevel::_updateCameraPosition ()
{
    int i = 0;

    glm::vec3 pl_size = glm::vec3 (0.f);
    float players_window[4] = {(float)this->ui_lvl_width, (float)this->ui_lvl_height, 0.f, 0.f}; //[left, top, right, bottom]

    //get the frame limits binding the players
    for (i = PLAYERS_COUNT; i--; )
    {
        if (!this->ap_characters[i])
        {
            _debug (WARNING, "Player%i is null, ignoring.", i);
            continue;
        }//end if (!this->ap_characters[i])

        pl_size = this->ap_characters[i]->getActiveSprite().getSize().getScaled ();

        if (this->v3pa_character_position[i]->x < players_window[0])
            players_window[0] = this->v3pa_character_position[i]->x;

        if (this->v3pa_character_position[i]->y < players_window[1])
            players_window[1] = this->v3pa_character_position[i]->y;

        if (this->v3pa_character_position[i]->x + pl_size.x > players_window[2])
            players_window[2] = this->v3pa_character_position[i]->x + pl_size.x;

        if (this->v3pa_character_position[i]->y + pl_size.y > players_window[3])
            players_window[3] = this->v3pa_character_position[i]->y + pl_size.y;
    }//end for (i = PLAYERS_COUNT; i--; )

    //find center point between players
    //keep aspect ratio
    this->p_view_window->frameByWidth ((players_window[0] + players_window[2]) / 2.f,
                                       (players_window[1] + players_window[3]) / 2.f,
                                       (players_window[2] - players_window[0] < MIN_CAMERA_WINDOW_WIDTH ?
                                                        MIN_CAMERA_WINDOW_WIDTH : players_window[2] - players_window[0]));

    this->p_common_shader->setCameraView (this->p_view_window->getViewMatrix ());
}//End updateCameraPosition ()


/**
 * @brief Process high priority events pertinent to the Level being played.
 *
 * @param obj_this - pointer to the DemoLevel instance.
 * @param ev - the event that was triggered.
 */
void DemoLevel::highPriorityEvents_cb (DemoLevel *obj_this, const EventManager::event_s &ev)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    switch (ev.type)
    {
        case EventManager::event_type_e::GAME:
        {
            static game_event_info_s *game_ev = nullptr;

            game_ev = (game_event_info_s *)ev.p_data;

            switch (game_ev->game_ev_id)
            {
                case ex_game_event_id_e::EV_CHARACTER_WALK_STEP:
                    obj_this->p_char_walk_step->getPosition().set (game_ev->v3_pos);
                    obj_this->p_audio_instance->play (obj_this->p_char_walk_step);
                    break;
                    //end case game_event_id_e::EV_CHARACTER_LANDED:

                case ex_game_event_id_e::EV_CHARACTER_RUN_STEP:
                    obj_this->p_char_run_step->getPosition().set (game_ev->v3_pos);
                    obj_this->p_audio_instance->play (obj_this->p_char_run_step);
                    break;
                    //end case game_event_id_e::EV_CHARACTER_LANDED:

                case ex_game_event_id_e::EV_CHARACTER_LANDED:
                    obj_this->p_char_land->getPosition().set (game_ev->v3_pos);
                    obj_this->p_audio_instance->play (obj_this->p_char_land);
                    break;
                    //end case game_event_id_e::EV_CHARACTER_LANDED:

                default:
                    break;
            }//end switch (game_ev->game_ev_id)
            break;
        }//end case EventManager::event_type::GAME:

        default:
            //something not expected, ignore it.
            _debug (WARNING, "Unexpected event -> %u", ev.type);
            break;
    }//end switch (ev._type)
}//End highPriorityEvents_cb ()


/**
 * @brief Process events pertinent to the Level being played.
 *
 * @param delta_t - time elapsed since last game loop iteration.
 */
void DemoLevel::processEvents (double delta_t)
{
    static const EventManager::event_s *ev = nullptr;

    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    while ((ev = this->p_ev_mgr_instance->pop (this->id_normal_ev_listener)))
    {
        switch (ev->type)
        {
            case EventManager::event_type_e::WINDOW:
            {
                static window_event_info_s *win_ev = nullptr;

                win_ev = (window_event_info_s *)ev->p_data;

                if (win_ev->window_ev_id == SDL_WINDOWEVENT_SIZE_CHANGED)
                {
                    this->p_common_shader->setOrthographicProjection (0.f, 0.f, win_ev->i_data1, win_ev->i_data2);
                    this->p_view_window->setWindowSize (win_ev->i_data1, win_ev->i_data2);
                }//end else if (EventManager::isEqual (ev, this->_window_size_changed))

                break;
            }//end case EventManager::event_type::WINDOW:

            default:
                //something not expected, ignore it.
                _debug (WARNING, "Unexpected event -> %u", ev->type);
                break;
        }//end switch (ev->_type)
    }//end while (this->evMngInstance.fetchEvent (this->lsID))
}//End processEvents ()


/**
 * @brief Updates anything Level related that is not a GameObject.
 *
 * @param delta_t - time elapsed since last game loop iteration.
 */
void DemoLevel::update (double delta_t)
{
    static glm::vec3 pl_size = glm::vec3 (0.f);
    static Camera2D::frame_limits camera_window;

    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    switch (this->getState ())
    {
        case level_state_e::LEVEL_LOADING:
            /*** go thru the loading animation untill state change. ***/
            this->_loading_animation.update (delta_t);
            this->ap_characters[0]->update (delta_t);
            break;
        //end case level_state_e::LEVEL_LOADING:

        case level_state_e::EXIT_THREAD:
            /*** need to do it only once and it can't be on the setState method or it would deadlock the working thread. ***/

            this->_joinThread ();

            /* finish setting up some resources on the main thread */
            this->background->init ();

            /* add game objects to the list in printing order */
            this->v_objects_list.push_back (this->background);

            for (auto pl_char : this->ap_characters)
                this->v_objects_list.push_back (pl_char);

            //carry on...
            this->setState (level_state_e::STARTING);
            break;
        //end case level_state_e::EXIT_THREAD:

        case level_state_e::STARTING:

            /*** go thru starting sequence and change to playing. ***/

            for (Character *pl_char : this->ap_characters)
                pl_char->setState (character_state_t::IDLE);

            this->setState (level_state_e::PLAYING);
            break;
        //end case level_state_e::STARTING:

        case level_state_e::PAUSE:
            break;

        case level_state_e::PLAYING:
        {
            int i = 0;

            //just go through all the registered GameObjetcs in order.
            for (GameObject *g_obj : this->v_objects_list)
                g_obj->update (delta_t);

            //update the level state  itself
            camera_window = this->p_view_window->getVisibleWorldLimits ();
            //update the microfone position to between the players.
            this->p_audio_instance->getListenerPosition().set (camera_window.f_x1 + (camera_window.f_x2 - camera_window.f_x1) / 2,
                                                               camera_window.f_y1 + (camera_window.f_y2 - camera_window.f_y1) / 2);

            //don't let player get out of the frame!
            //the camera itself will prevent the frame from showing outside the level.
            for (i = PLAYERS_COUNT; i--; )
            {
                if (!this->ap_characters[i])
                {
                    _debug (WARNING, "Player%i is null, ignoring.", i);
                    continue;
                }//end if (!this->ap_characters[i])

                pl_size = this->ap_characters[i]->getActiveSprite().getSize().getScaled ();

                //prevent players from exiting the world space
                if (this->v3pa_character_position[i]->x < 0.f)
                    this->ap_characters[i]->getPosition().set (0.f,
                                                               this->v3pa_character_position[i]->y,
                                                               this->v3pa_character_position[i]->z);

                else if (this->v3pa_character_position[i]->x + pl_size.x >= this->ui_lvl_width)
                    this->ap_characters[i]->getPosition().set (this->ui_lvl_width - pl_size.x,
                                                               this->v3pa_character_position[i]->y,
                                                               this->v3pa_character_position[i]->z);
                //prevent players from exiting the camera frame
                //cast to int to prevent float rounding errors comparing to zero!
                else if (static_cast<int>(camera_window.f_y1) <= 0) //can't pan out anymore!
                {
                    ///@bug Player on the left can move the camera when pan out is max! 0.1 shaves a little, but not a solution!
                    if (this->v3pa_character_position[i]->x < camera_window.f_x1)
                        this->ap_characters[i]->getPosition().set (camera_window.f_x1 + 0.1,
                                                                   this->v3pa_character_position[i]->y,
                                                                   this->v3pa_character_position[i]->z);

                    else if (this->v3pa_character_position[i]->x + pl_size.x >= camera_window.f_x2)
                        this->ap_characters[i]->getPosition().set (camera_window.f_x2 - pl_size.x,
                                                                   this->v3pa_character_position[i]->y,
                                                                   this->v3pa_character_position[i]->z);
                }//end if (camera_window.f_y1 <= 0.f)
            }//end for (i = PLAYERS_COUNT; i--; )

            this->_updateCameraPosition ();
            break;
        }//end case level_state_e::PLAYING:

        case level_state_e::ENDING:
            //go thru ending sequence and change to finish
            this->setState (level_state_e::FINISHED);
            break;

        case level_state_e::FINISHED:
            return; //<-- just get out, shouldn't be here

        default:
            break;
    }//end switch (this->getState ())
}//End update ()


/**
 * @brief Draws anything Level related that is not a GameObject.
 *
 * @param delta_t - time elapsed since last game loop iteration.
 */
void DemoLevel::draw (double delta_t)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    switch (this->getState ())
    {
        case level_state_e::LEVEL_LOADING:
            /*** go thru the loading animation untill state change. ***/
            this->_loading_animation.draw (*this->p_common_shader);
            this->ap_characters[0]->draw ();

            this->sp_textbox.draw (*this->p_common_shader);
            break;
        //end case level_state_e::LEVEL_LOADING:

        case level_state_e::STARTING:
        case level_state_e::PAUSE:
        case level_state_e::PLAYING:
        case level_state_e::ENDING:
            //just go through all the registered GameObjetcs in order.
            for (auto g_obj : this->v_objects_list)
                g_obj->draw ();
            break;
        //end case level_state_e::STARTING:

        default:
            break;
    }//end switch (this->getState ())
}//End draw ()

